package main

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"html"
	"html/template"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	texttemplate "text/template"
	"time"

	"github.com/gin-gonic/gin"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	// URL(Uniform Resource Locator)是“统一资源定位符”的英文缩写，定义如下
	//                                        scheme://host[:port#]/path/.../[?query-string][#anchor]
	// scheme 指定低层使用的协议(例如：http, https, ftp)
	// host HTTP服务器的IP地址或者域名
	// port# HTTP服务器的默认端口是80，这种情况下端口号可以省略。如果使用了别的端口，必须指明，例如 http://www.path 访问资源的路径
	// query-string 发送给http服务器的数据
	// anchor 锚

	r.ParseForm()                        //解析参数，默认是不会解析的
	fmt.Println(r.Form)                  //这些信息是输出到服务器端的打印信息
	fmt.Println("methods:", r.Method)    //http 请求命令
	fmt.Println("path:", r.URL.Path)     //打印路径
	fmt.Println("scheme:", r.URL.Scheme) //打印协议类型 希望会打印出 http  但测试并没有而是为空
	fmt.Println(r.Form["url_long"])      //打印url_long值 http://localhost:9090/?url_long=111&url_long=222 会向url_long输入两个数据
	for k, v := range r.Form {
		fmt.Println("key:", k)
		fmt.Println("val:", strings.Join(v, ","))
	}
	fmt.Fprintf(w, "<h1>Hello go web!</h1>") //这个写入到w的是输出到客户端的
}

//当用户名输入 <script>alert()</script> 这样的脚字符串时，就有可能会产生 跨站脚本注入攻击(cross site script)
//简称 XSS，应对的最简单办法是，对动态生成的html，进行字符串特殊字符转义 例如 <>
func escapeXSS(str string) (ret string) {
	defer func() {
		err := recover()
		if err != nil {
			ret = ""
		}
	}()
	ret = html.EscapeString(str)
	return ret
}

func login(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method:", r.Method) //获取请求的方法
	if r.Method == "GET" {
		//以当前的时间做md5 hash 作为token值
		currenttime := time.Now().Unix()
		h := md5.New() //新建一个 md5 hash累计计算器,对它写入字符串，就相当于计算hash值了
		io.WriteString(h, strconv.FormatInt(currenttime, 10))
		token := h.Sum(nil) //通过sum(nil) 返回h当前累计的hash值
		// token1 := md5.Sum([]byte(strconv.FormatInt(currenttime, 10)))	//这样也是可以生成 hash的，但是它是一次性返回 hash值，不能再加入字节进行计算

		stoken := fmt.Sprintf("%x", token) //将16字节的 hash值 变换为16进制字符串

		t, _ := template.ParseFiles("login.gtpl")
		t.Execute(w, stoken) //将token加载到模板中

		//读取cookie
		cookie, _ := r.Cookie("logintoken")
		if cookie != nil && cookie.Value != "" {
			fmt.Println("读取到cookie 上次登录的token为：", cookie.Value)
			fmt.Fprintf(w, "<h5>读取到cookie,上次登录使用的token = %v</h5>", cookie.Value)
		}
		// //还有另外一种读取方式
		// for _, cookie := range r.Cookies() {
		// 	fmt.Fprint(w, cookie.Name)
		// }

	} else if r.Method == "POST" {
		//请求的是登陆数据，那么执行登陆的逻辑判断
		r.ParseForm()

		token := r.Form.Get("token")
		if token != "" {
			//验证token的合法性
			fmt.Println("接收到 token 值： ", token)

			//
			username := escapeXSS(r.Form.Get("username"))
			password := escapeXSS(r.Form.Get("password"))
			if username != "" && password != "" {
				//cookie 记录已经登录
				//expiration := time.Now().Add(365 * 24 * time.Hour)  //设置cookie过期时间
				cookie := http.Cookie{Name: "logintoken", Value: token} //, Expires: expiration}
				http.SetCookie(w, &cookie)

				v := url.Values{}
				v.Set("username", username)
				v.Set("password", password)
				//返回重定向 回复
				//第三个参数为 回复状态码 有下面的值：
				// 1XX 提示信息 - 表示请求已被成功接收，继续处理
				// 2XX 成功 - 表示请求已被成功接收，理解，接受
				// 3XX 重定向 - 要完成请求必须进行更进一步的处理
				// 4XX 客户端错误 - 请求有语法错误或请求无法实现
				// 5XX 服务器端错误 - 服务器未能实现合法的请求
				http.Redirect(w, r, "/welcome?"+v.Encode(), http.StatusFound)
				return
			}

		} else {
			//不存在token报错
			fmt.Println("不存在 token 值 ")
			return
		}

		fmt.Println("username:", r.Form["username"])
		fmt.Println("password:", r.Form["password"])
		// fmt.Fprintf(w, "<h1> 你好呀 ！%s </h1><br><p>你输入的密码是：%s </p> ", r.Form["username"][0], r.Form["password"][0])
		fmt.Fprintf(w, "<h1> 你好呀 ！%s </h1><br><p>你输入的密码是：%s </p> ",
			escapeXSS(r.Form.Get("username")),
			escapeXSS(r.Form.Get("password"))) //防止XSS攻击

		//模版的使用
		fmt.Fprintf(w, " <br> ")
		t, _ := texttemplate.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) //text/template使用
		t.ExecuteTemplate(w, "T", "<script>alert('text/template使用')</script>")

		fmt.Fprintf(w, " <br> ")
		th, _ := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) //html/template的使用
		th.ExecuteTemplate(w, "T", "<script>alert('html/template的使用')</script>")

		// request.Form是一个url.Values类型，里面存储的是对应的类似key=value的信息，下面展示了可以对form数据进行的一些操作:
		v := url.Values{}
		v.Set("name", "Ava")
		v.Add("friend", "Jess")
		v.Add("friend", "Sarah")
		v.Add("friend", "Zoe")
		fmt.Println(v.Encode()) //这里编码的是按key排序后编码
		fmt.Println(v.Get("name"))
		fmt.Println(v.Get("friend"))
		fmt.Println(v["friend"])

	} else {
		http.NotFound(w, r)
	}
}

func welcome(w http.ResponseWriter, r *http.Request) {
	//请求的是登陆数据，那么执行登陆的逻辑判断
	r.ParseForm()

	//读取cookie
	cookie, _ := r.Cookie("logintoken")

	username := escapeXSS(r.Form.Get("username"))
	password := escapeXSS(r.Form.Get("password"))
	if cookie != nil && cookie.Value != "" && username != "" && password != "" {
		fmt.Println("读取到cookie 上次登录的token为：", cookie.Value)
		fmt.Println("上次登录的username为：", username)
		fmt.Println("上次登录的password为：", password)
		fmt.Fprintf(w, "<h1>欢迎  %v  登录 </h1><br><h2>你的密码为： %v</h2><br><h5>读取到cookie,上次登录使用的token = %v</h5><br><a href='/'>主页</a>",
			username, password, cookie.Value)
	} else {
		//返回重定向 登录
		http.Redirect(w, r, "/login", http.StatusFound)
	}
}

//cookie 结构
// type Cookie struct {
// 	Name       string
// 	Value      string
// 	Path       string
// 	Domain     string
// 	Expires    time.Time	//过期时间
// 	RawExpires string		//只读cookie
// 	// MaxAge=0 means no 'Max-Age' attribute specified.
// 	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// 	// MaxAge>0 means Max-Age attribute present and given in seconds
// 	MaxAge   int	//设置过期时间，对应浏览器cookie的MaxAge属性
// 	Secure   bool
// 	HttpOnly bool		//是否只用在http https，禁止如js等其它访问cookie
// 	Raw      string
// 	Unparsed []string // Raw text of unparsed attribute-value pairs
// }

func main() {

	switch "gin" {
	case "http":
		testEmbeddedTmpl()

		http.HandleFunc("/", sayhelloName)   //注册一个 http 服务 的url '/' 路径监听处理函数 到 DefaultServeMux默认路由处理对象
		http.HandleFunc("/login", login)     //注册 /login 路径的hander函数
		http.HandleFunc("/welcome", welcome) //注册 /welcome 路径的hander函数
		http.Handle("/static/",
			http.StripPrefix("/static/", http.FileServer(http.Dir("static/")))) //注册静态资源处理

		fmt.Println("http server at http://localhost:9090")
		err := http.ListenAndServe(":9090", nil) //设置http 服务 监听的地址和端口；handler参数通常为空，通常用于路由连接，为nil时使用http包默认路由 DefaultServeMux
		if err != nil {
			log.Fatal("ListenAndServe: ", err)
		}
	case "gin":
		//log 日志写到文件
		func(islogfile bool) {
			if islogfile {
				// 禁止控制台颜色字符, 日志写到文件是不需要颜色符的.
				gin.DisableConsoleColor()
				//gin.ForceConsoleColor() //可以打开颜色

				// 创建并设置日志写入文件
				f, _ := os.Create("gin.log")
				gin.DefaultWriter = io.MultiWriter(f)

				// 下面代码使写入文件的同时也输出到标准输出
				// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
			}
		}(false)

		//1.创建路由
		// r := gin.Default()

		r := gin.New() //无中间件空白gin 没有log 没有Recovery
		// 作用于全局默认logger recovery
		r.Use(gin.Logger())
		r.Use(gin.Recovery())

		// 自定义中间件 及 作用于某个组的中间件
		gcustomlogger := r.Group("/customlogger")

		gcustomlogger.Use(
			gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { //自定义log格式化输出
				// your custom format
				return fmt.Sprintf("customlogger | %s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
					param.ClientIP,
					param.TimeStamp.Format(time.RFC1123),
					param.Method,
					param.Path,
					param.Request.Proto,
					param.StatusCode,
					param.Latency,
					param.Request.UserAgent(),
					param.ErrorMessage,
				)
			}))
		{
			gcustomlogger.GET("/", func(c *gin.Context) {
				c.String(http.StatusOK, "这里是 %s", c.FullPath())
			})
			gcustomlogger.GET("/test", func(c *gin.Context) {
				c.String(http.StatusOK, "这里是 %s", c.FullPath())
			})
		}

		//2.绑定路由规则，执行的函数
		r.GET("/sayhello", func(c *gin.Context) {
			c.Header("Content-Type", "text/html; charset=utf-8") //加上这句 指明下面的 都是html内容,content-type是指http协议回复内容类型
			//content-type 常见列表
			// 常见的媒体格式类型如下：
			// 	text/html ： HTML格式
			// 	text/plain ：纯文本格式
			// 	text/xml ： XML格式
			// 	image/gif ：gif图片格式
			// 	image/jpeg ：jpg图片格式
			// 	image/png：png图片格式
			// 以application开头的媒体格式类型：
			// 	application/xhtml+xml ：XHTML格式
			// 	application/xml： XML数据格式
			// 	application/atom+xml ：Atom XML聚合格式
			// 	application/json： JSON数据格式
			// 	application/pdf：pdf格式
			// 	application/msword ： Word文档格式
			// 	application/octet-stream ： 二进制流数据（如常见的文件下载）
			// 	application/x-www-form-urlencoded ： <form encType=””>中默认的encType，form表单数据被编码为key/value格式发送到服务器（表单默认的提交数据的格式）
			// 另外一种常见的媒体格式是上传文件之时使用的：
			// 	multipart/form-data ： 需要在表单中进行文件上传时，就需要使用该格式

			//cookie 获取和设置
			cookiev, err := c.Cookie("sayhello")
			if err == nil && cookiev != "" {
				c.String(http.StatusOK,
					fmt.Sprintf("<br><h6>设置的 cookie \"%s\"值： %s</h6>", "sayhello", cookiev))
			} else {
				c.SetCookie( //SetCookie 要在所有写 http内容回复之前调用
					"sayhello",    //name
					"cookie demo", //value
					0,             //maxAge	-1表示内存cookie  0表示删除cookie  为正数，则表示该Cookie会在maxAge秒之后自动失效
					"",            //path
					"",            //domain
					false,         //secure
					false)         //httpOnly

				c.String(http.StatusOK, "<br><h6>还没有设置cookie 请刷新</h6>")
			}

			//输出经过滤的字符串到 html  没有c.Header("Content-Type" "text/html) 默认是text/plain
			c.String(http.StatusOK,
				"<h1>Hello World!</h1><br><form action='somePost' method='post'>用户名:<input type='text' name='username'> <br><input type='submit' value='登陆'></form><br><br>")

			c.String(http.StatusOK,
				"<form action='someGet' method='get'>查找:<input type='text' name='find'> <br><input type='submit' value='查找'></form><br><br>")

			//输出json结果给调用方 没有c.Header("Content-Type")则会包裹在 <pre/>中
			c.JSON(http.StatusOK, gin.H{
				"message": "pong",
				"a": gin.H{
					"b": 1,
				},
			})

		})

		// GET, POST, PUT, PATCH, DELETE and OPTIONS 使用示例
		// router.GET("/someGet", getting)
		// router.POST("/somePost", posting)
		// router.PUT("/somePut", putting)
		// router.DELETE("/someDelete", deleting)
		// router.PATCH("/somePatch", patching)
		// router.HEAD("/someHead", head)
		// router.OPTIONS("/someOptions", options)

		r.GET("/someGet", func(c *gin.Context) {
			find := c.Query("find")              //没有时返回空
			user := c.DefaultQuery("user", "匿名") //带默认值的 query返回
			c.String(http.StatusOK, "%s 你要查找 %s", user, find)
		})

		r.POST("/somePost", func(c *gin.Context) {
			username := c.PostForm("username")
			password := c.DefaultPostForm("password", "000000") // 可设置默认值

			//Map参数(字典参数)
			//命令行测试 $ curl -g "http://localhost:9090/somePost?ids[Jack]=001&ids[Tom]=002" -X POST -d 'names[a]=Sam&names[b]=David'
			//回复 {"ids":{"Jack":"001","Tom":"002"},"names":{"a":"Sam","b":"David"}}
			ids := c.QueryMap("ids")
			names := c.PostFormMap("names")

			c.JSON(http.StatusOK, gin.H{
				"username": username,
				"password": password,
				"ids":      ids,
				"names":    names,
			})
		})

		//重定向
		r.GET("/redirect", func(c *gin.Context) {
			c.Redirect(http.StatusMovedPermanently, "/sayhello")
		})

		//分组路由(Grouping Routes)
		//如果有一组路由，前缀都是/api/v1开头，是否每个路由都需要加上/api/v1这个前缀呢？
		//答案是不需要，分组路由可以解决这个问题。
		//利用分组路由还可以更好地实现权限控制，
		//例如将需要登录鉴权的路由放到同一分组中去，简化权限控制
		// group routes 分组路由
		defaultHandler := func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{
				"path": c.FullPath(),
			})
		}
		// group: v1
		v1 := r.Group("/v1")
		{
			v1.GET("/posts", defaultHandler)
			v1.GET("/series", defaultHandler)
		}

		// group: v2
		v2 := r.Group("/v2")
		{
			v2.GET("/posts", defaultHandler)
			v2.GET("/series", defaultHandler)
		}

		//大括号 作用域
		x := 1
		fmt.Println(x)
		{ //这里的大括号  相当于一个新的作用域，里面可以新建x
			x := 2
			fmt.Println(x)
			func() {
				x = x + 1 //x不是函数里的，但在可以防问到，一种非全局变量多个函数访问的方法
				fmt.Println(x)
			}()
			func() {
				x = x + 1
				fmt.Println(x)
			}()
		}

		//路径参数获取
		// 可以匹配到 /urlparam/john ,但不能匹配到 /urlparam/ 或 /urlparam
		r.GET("/urlparam/:name", func(c *gin.Context) {
			name := c.Param("name")
			c.String(http.StatusOK, "/urlparam/:name 匹配到 name = "+name)
		})

		// 下面的代码可以匹配到 /urlparam/john/ 和 /urlparam/john/send
		// 如果没有其它路由匹配到 /urlparam/john, 将会重定向到 /urlparam/john/
		r.GET("/urlparam/:name/*action", func(c *gin.Context) {
			name := c.Param("name")
			action := c.Param("action")
			message := "/urlparam/:name/*action 匹配到 name = " + name + "  action =  " + action
			c.String(http.StatusOK, message)

			//对url匹配到的post请求，context会保留匹配定义
			ismatch := c.FullPath() == "/urlparam/:name/*action" // true
			c.String(http.StatusOK,
				"\nc.FullPath() == \"/urlparam/:name/*action\" 的结果是 "+strconv.FormatBool(ismatch))
		})

		//html模版
		// r.LoadHTMLGlob("templates/*") //加载目录下的所有模板,加载后 c.HTML()只需要输入文件名
		r.LoadHTMLGlob("login.gtpl")
		r.GET("/login", func(c *gin.Context) {
			c.HTML(http.StatusOK, "login.gtpl", gin.H{
				"tokenname": "token",
				"value":     1234567,
			})
		})

		// //自定义html模板
		// html := template.Must(template.ParseFiles("file1", "file2"))
		// r.SetHTMLTemplate(html)

		// //自定义 html模板分隔符示例
		// r.Delims("{[{", "}]}") //这个要放在所有模板加载之前
		// r.LoadHTMLGlob("/path/to/templates")

		//数据绑定
		//分为MustBind 和 ShouldBind两类，前者绑定失败panic,后者返回err
		//详细用法见 https://pkg.go.dev/github.com/gin-gonic/gin#readme-model-binding-and-validation
		r.POST("/login", func(c *gin.Context) {
			logindata := struct {
				User     string `form:"username" json:"username" xml:"username"  binding:"required"`
				Password string `form:"password" json:"password" xml:"password" binding:"required"`
			}{}
			// //测试命令 curl -v -X POST  http://localhost:9090/login -H 'content-type: application/json' -d '{ "username": "manu","password":"123" }'
			// if err := c.ShouldBindJSON(&logindata); err != nil {
			// 	c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			// 	return
			// }

			//测试命令 curl -g "http://127.0.0.1:9090/login?username=user&password=123" -X POST -d 'username=myname'
			err := c.ShouldBind(&logindata) //自动识别绑定可用的数据 form json xml等
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}

			// if json.User != "manu" || json.Password != "123" {		//这里返回登录验证失败 401
			// 	c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			// 	return
			// }

			c.JSON(http.StatusOK, gin.H{"username": logindata.User, "password": logindata.Password})

			//结构体渲染为json数据
			renderjsondata := struct {
				Name  string `json:"jsonname"`
				Value string `json:"jsonvalue"`
			}{
				Name:  "我是名字",
				Value: "我是值",
			}
			c.JSON(http.StatusOK, renderjsondata)
		})

		//静态资源服务
		r.Static("/static", "./static") //gin.Dir()与http.FileServer()的合成
		// r.StaticFS("/more_static", http.Dir("my_file_system")) //与Static相似但是可以自定义文件系统接口
		r.StaticFile("/log", "./gin.log") //单一文件静态服务
		//从文件获取服务数据
		//除下面两种方法外  还可以自定义读取器读取数据响应服务
		//详细见 https://pkg.go.dev/github.com/gin-gonic/gin#readme-serving-data-from-reader
		r.GET("/local/vue", func(c *gin.Context) {
			c.File("static/testVue.html")
		})
		r.GET("/local/vuetify", func(c *gin.Context) {
			var fs http.FileSystem = http.Dir("static")
			c.FileFromFS("testVuetify.html", fs) //人文件系统获取数据
		})

		//3.监听端口，默认9090
		r.Run(":9090")
	}
}

//嵌套模板
func testEmbeddedTmpl() {
	th := `
{{define "header"}}
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{end}}
	`
	//{{template "header" .}} 最后面的点表示向模板转参
	//{{index .Content 0}} 中的index 是 template.FuncMap内置命令，用于索引。template.FuncMap可以自定义go函数
	tc := `
{{template "header" .}}
<h1>演示嵌套</h1>
<p>{{.Content}}</p>
<p>第一个元素是: {{index .Content 0}}</p>
<ul>
<li>嵌套使用define定义子模板</li>
<li>调用使用template</li>
</ul>
{{template "footer" .}}
		`
	tf := `
{{define "footer"}}
&lt;{{.Footer}}&gt;
</body>
</html>
{{end}}
			`

	data := struct { //匿名结构体
		Title   string
		Content []string
		Footer  string
	}{
		Title:   "嵌套模板",
		Content: []string{"内容1", "内容2", "内容3"},
		Footer:  "这里是页脚",
	}

	t := template.Must( //Must 的作用是 模板必须正确  否则会panic
		template.New("embeddedTmpl").Parse(th + tc + tf))

	// template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")	//本来是从文件解释的

	//t.Execute() 全局执行模板

	t.ExecuteTemplate(os.Stdout, "header", data) //执行命名模板  每一次写os.Stdout都会打印输出
	// t.ExecuteTemplate(os.Stdout, "content", data) //执行命名模板
	t.ExecuteTemplate(os.Stdout, "footer", data) //执行命名模板

	//执行的模板结果写到缓冲
	buf := new(bytes.Buffer)
	// t.ExecuteTemplate(os.Stdout, "content", data) //由于content里的引用了 header 和 footer 所以就一步到位了
	t.Execute(buf, data)

	fmt.Println(buf.String())

}
